/*
 * Copyright 2011 The Rusted Roof, LLC.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.proteusframework.platformservice;

import org.proteusframework.core.api.IProteusConstants;
import org.proteusframework.core.runtime.DefaultRuntimeConfiguration;
import org.proteusframework.core.util.Assert;
import org.proteusframework.platformservice.appconfig.IAppConfigService;
import org.proteusframework.platformservice.lifecycle.api.ILifecycleService;
import org.proteusframework.platformservice.lifecycle.api.LifecyclePhase;
import org.proteusframework.platformservice.lifecycle.api.LifecycleServiceIntention;
import org.proteusframework.platformservice.lifecycle.api.LifecycleServiceIntentionEvent;
import org.proteusframework.platformservice.messaging.IMessageService;
import org.proteusframework.platformservice.persistence.api.IPersistenceService;
import org.proteusframework.platformservice.theme.IThemeService;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

@SuppressWarnings("unchecked")
/**
 * Provides a default extended runtime implementation of the Proteus Framework suitable for use with the Proteus
 * Platform Services. Most extended applications that require platform services can simply rely on this default
 * runtime configuration.
 *
 * @author Tacoma Four
 */
public final class ExtendedRuntimeConfiguration extends DefaultRuntimeConfiguration implements IExtendedRuntimeConfiguration
{
    private static final Logger logger = Logger.getLogger(ExtendedRuntimeConfiguration.class.getCanonicalName());

    private ILifecycleService lifecycleService;
    private IPersistenceService persistenceService;
    private IThemeService themeService;
    private IMessageService messageService;
    private IAppConfigService appConfigService;

    private Map<Integer, Object> customServiceMap = new HashMap<Integer, Object>();

    /**
     * Lock used to guarantee that we aren't both setting a new active platform service and requesting the
     * active platform service simultaneously.
     */
    private final Object lock = new Object();

    @Override
    protected void onDelegateInjected()
    {
        //
        // Let the super class find the /proteus.properties file for us
        super.onDelegateInjected();

        //
        // Load Lifecycle Platform Service, Must Exist!
        //
        String lifecycleServiceClass = proteusProperties.getProperty(KEY_LIFECYCLE_SERVICE);
        logger.config("Initializing lifecycle service found in " + lifecycleServiceClass);

        StringBuilder lifecycleBuf = new StringBuilder("Proteus Framework Platform Service applications must define ")
                .append("a Lifecycle Platform Service implementation under the key ")
                .append(KEY_LIFECYCLE_SERVICE)
                .append(" within a ")
                .append(IProteusConstants.PROTEUS_BOOTSTRAP_PROPERTIES)
                .append(" file that can be found in the root of the CLASSPATH");

        if (null == lifecycleServiceClass)
        {
            logger.severe(lifecycleBuf.toString());
            System.exit(-2);
        }

        try
        {
            lifecycleService = (ILifecycleService) initializeService(lifecycleServiceClass, ILifecycleService.class);
            lifecycleService.start();
        } catch (ServiceInitializationException e)
        {
            logger.severe(lifecycleBuf.toString());
            System.exit(-2);
        }

        //
        // Load App Config Platform Service, Must Exist!
        //
        String appConfigServiceClass = proteusProperties.getProperty(KEY_APP_CONFIG_SERVICE);
        logger.config("Initializing app config service found in " + appConfigServiceClass);

        StringBuilder appCfgBuf = new StringBuilder("Proteus Framework Platform Service applications must define ")
                .append("an Application Configuration Platform Service implementation under the key ")
                .append(KEY_APP_CONFIG_SERVICE)
                .append(" within a ")
                .append(IProteusConstants.PROTEUS_BOOTSTRAP_PROPERTIES)
                .append(" file that can be found in the root of the CLASSPATH");

        if (null == appConfigServiceClass)
        {
            logger.severe(appCfgBuf.toString());
            System.exit(-1);
        }

        try
        {
            appConfigService = (IAppConfigService) initializeService(appConfigServiceClass, IAppConfigService.class);
            appConfigService.start();
        } catch (ServiceInitializationException e)
        {
            logger.severe(appCfgBuf.toString());
            System.exit(-1);
        }

        //
        // Load Persistence Platform Service, if exists
        //
        String persistenceServiceClass = proteusProperties.getProperty(KEY_PERSISTENCE_SERVICE);
        if (null != persistenceServiceClass)
        {
            try
            {
                persistenceService =
                        (IPersistenceService) initializeService(persistenceServiceClass, IPersistenceService.class);

                activateService(ServiceClassification.Persistence, persistenceService, IPersistenceService.class);
            } catch (ServiceInitializationException e)
            {
                logger.severe("Defined IPersistenceService not initialized and is unavailable to the application");
            }
        } else
        {
            logger.config("No persistence service was declared in the /proteus.properties bootstrap file");
        }

        //
        // Load Messaging Platform Service, if exists
        //
        String messagingServiceClass = proteusProperties.getProperty(KEY_MESSAGE_SERVICE);
        if (null != messagingServiceClass)
        {
            try
            {
                messageService =
                        (IMessageService) initializeService(messagingServiceClass, IMessageService.class);

                activateService(ServiceClassification.Messaging, messageService, IMessageService.class);
            } catch (ServiceInitializationException e)
            {
                logger.severe("Defined IMessageService not initialized and is unavailable to the application");
            }
        } else
        {
            logger.config("No messaging service was declared in the /proteus.properties bootstrap file");
        }

        //
        // Load Theme Platform Service, if exists
        //
        String themeServiceClass = proteusProperties.getProperty(KEY_THEME_SERVICE);
        if (null != themeServiceClass)
        {
            try
            {
                themeService = (IThemeService) initializeService(themeServiceClass, IThemeService.class);

                activateService(ServiceClassification.Theme, themeService, IThemeService.class);
            } catch (ServiceInitializationException e)
            {
                logger.severe("Defined IThemeService not initialized and is unavailable to the application");
            }
        } else
        {
            logger.config("No theme service was declared in the /proteus.properties bootstrap file");
        }

        //
        // Load Custom Platform Services
        //
        for (String propName : proteusProperties.stringPropertyNames())
        {
            if (!propName.startsWith(PROTEUS_SERVICE_KEY_PREFIX))
            {
                String customServiceClass = proteusProperties.getProperty(propName);
                if (null != customServiceClass)
                {
                    try
                    {
                        initializeService(customServiceClass, IServicePlugin.class);
                    } catch (ServiceInitializationException e)
                    {
                        this.delegate
                                .getEngine(IExtendedRuntimeEngine.class)
                                .onCustomPlatformServiceInitializationException(e);
                    }
                }
            }
        }
    }

    private <T extends IServicePlugin> IServicePlugin initializeService(String className,
                                                                        Class<T> serviceClass)
            throws ServiceInitializationException
    {
        IServicePlugin result = null;

        try
        {
            Class<? extends IServicePlugin> clazz =
                    Class.forName(className).asSubclass(IServicePlugin.class);

            if (serviceClass.isAssignableFrom(clazz))
            {
                result = clazz.newInstance();

                if (!className.startsWith(PROTEUS_SERVICE_KEY_PREFIX))
                {
                    logger.config("Loaded platform service found in " + serviceClass);
                    Integer customServiceHashCode = clazz.hashCode();
                    result.setPlatformDelegate(delegate, IExtendedPlatformDelegate.class);
                    result.start();
                    customServiceMap.put(customServiceHashCode, result);
                }
            }

        } catch (ClassNotFoundException e)
        {
            String msg = "Cannot find class " + className + " on the CLASSPATH";
            logger.severe(msg);
            throw new ServiceInitializationException(msg, e);
        } catch (InstantiationException e)
        {
            String msg = "Cannot instantiate class " + className + " found on the CLASSPATH";
            logger.severe(msg);
            throw new ServiceInitializationException(msg, e);
        } catch (IllegalAccessException e)
        {
            String msg = "Cannot access class " + className;
            logger.severe(msg);
            throw new ServiceInitializationException(msg, e);
        }

        return result;
    }

    /**
     * Invokes the {@link IServicePlugin#start()} method on the instance of the
     * platform service plugin, tearing down the previously active service (if it existed) by invoking its
     * {@link IServicePlugin#stop()} method.
     *
     * @param serviceClassification Platform service to locate
     * @param newInstance           Instance of the given platform service, implement the specified serviceInterface. To
     *                              permanently terminate a platform service, pass a null.
     * @param serviceInterface      Expected interface of the given platform service
     * @return true, if the platform service was successfully activated
     * @throws IllegalStateException when the
     */
    public <T extends IServicePlugin> boolean activateService(ServiceClassification serviceClassification,
                                                              Object newInstance,
                                                              Class<T> serviceInterface)
    {
        Assert.parameterNotNull(serviceInterface, "Parameter 'serviceInterface' must not be null");
        Assert.parameterNotNull(newInstance, "Parameter 'newInstance' must not be null");
        Assert.parameterNotNull(serviceClassification, "Parameter 'serviceClassification' must not be null");

        boolean result = true;
        Integer serviceHashCode = serviceInterface.hashCode();
        IServicePlugin oldService = null;

        switch (serviceClassification)
        {
            case Lifecycle:
                throw new IllegalStateException("Lifecycle service cannot be changed dynamically");
            case Persistence:
                oldService = persistenceService;
                break;
            case Messaging:
                oldService = messageService;
                break;
            case Theme:
                oldService = themeService;
                break;
            case ApplicationConfiguration:
                throw new IllegalStateException("Application Configuration service cannot be changed dynamically");
            case Custom:
                if (customServiceMap.containsKey(serviceHashCode))
                {
                    oldService = serviceInterface.cast(customServiceMap.get(serviceHashCode));
                }
                break;
        }

        LifecycleServiceIntention stopIntention = (null == newInstance)
                ? LifecycleServiceIntention.TerminateService
                : LifecycleServiceIntention.ReplaceService;

        synchronized (lock)
        {
            if (null != oldService)
            {
                firePreEvent(serviceClassification, stopIntention, oldService);

                try
                {
                    if (logger.isLoggable(Level.FINE))
                    {
                        logger.fine("Stopping the currently installed " + serviceClassification);
                    }

                    oldService.stop();

                    if (logger.isLoggable(Level.FINE))
                    {
                        logger.fine("Stopped the currently installed " + serviceClassification);
                    }

                } catch (Exception e)
                {
                    StringBuilder buf = new StringBuilder("Unable to properly stop() active ")
                            .append(serviceClassification)
                            .append("\n\n")
                            .append("setActivePlatformService Exception on stop() in ")
                            .append(oldService.getFamily())
                            .append(" ")
                            .append(oldService.getId())
                            .append(":\n")
                            .append(e.getMessage());

                    logger.severe(buf.toString());
                }

                firePostEvent(serviceClassification, stopIntention, oldService);
            }


            // TODO: How do we transfer state between the outgoing and incoming service?
            logger.warning("Presently the framework does not provide a state transfer mechanism between transitioning services");

            switch (serviceClassification)
            {
                case Persistence:
                    persistenceService = null;
                    break;
                case Messaging:
                    messageService = null;
                    break;
                case Theme:
                    themeService = null;
                    break;
                case Custom:
                    customServiceMap.remove(serviceHashCode);
                    break;
            }

            if (null != newInstance)
            {
                IServicePlugin newService = IServicePlugin.class.cast(newInstance);

                firePreEvent(serviceClassification, LifecycleServiceIntention.InstallService, newService);

                try
                {

                    if (logger.isLoggable(Level.FINE))
                    {
                        logger.fine("Starting the new " + serviceClassification);
                    }

                    newService.start();

                    if (logger.isLoggable(Level.FINE))
                    {
                        logger.fine("Started the new " + serviceClassification);
                    }

                    switch (serviceClassification)
                    {
                        case Persistence:
                            persistenceService = (IPersistenceService) newService;
                            break;
                        case Messaging:
                            messageService = (IMessageService) newService;
                            break;
                        case Theme:
                            themeService = (IThemeService) newService;
                            break;
                        case Custom:
                            customServiceMap.put(serviceHashCode, newService);
                            break;
                    }

                } catch (Exception e)
                {
                    StringBuilder buf = new StringBuilder("Unable to properly start() new ")
                            .append(serviceClassification)
                            .append("\n\n")
                            .append("setActivePlatformService Exception on start() in ")
                            .append(newService.getFamily())
                            .append(" ")
                            .append(newService.getId())
                            .append(":\n")
                            .append(e.getMessage());

                    logger.severe(buf.toString());
                }

                firePostEvent(serviceClassification, LifecycleServiceIntention.InstallService, newService);

            }
        }

        return result;
    }

    private void firePreEvent(ServiceClassification serviceClassification,
                              LifecycleServiceIntention intention,
                              IServicePlugin service)
    {
        logger.config(serviceClassification + " {serviceIntention=" + intention + ", phase=" + LifecyclePhase.Pre + "}");

        LifecycleServiceIntentionEvent<IServicePlugin> event =
                new LifecycleServiceIntentionEvent<IServicePlugin>(service,
                        serviceClassification,
                        intention,
                        LifecyclePhase.Pre);

        lifecycleService.fireLifecycleEvent(event);
    }

    private void firePostEvent(ServiceClassification serviceClassification,
                               LifecycleServiceIntention intention,
                               IServicePlugin service)
    {
        logger.config(serviceClassification + " {serviceIntention=" + intention + ", phase=" + LifecyclePhase.Post + "}");

        LifecycleServiceIntentionEvent<IServicePlugin> event =
                new LifecycleServiceIntentionEvent<IServicePlugin>(service,
                        serviceClassification,
                        intention,
                        LifecyclePhase.Pre);

        lifecycleService.fireLifecycleEvent(event);
    }

    @Override
    public <T extends IServicePlugin> T getPlatformService(ServiceClassification serviceClassification, Class<T> serviceInterface)
            throws ServiceNotInstalledException
    {
        T result = null;

        synchronized (lock)
        {
            switch (serviceClassification)
            {
                case Lifecycle:
                    result = serviceInterface.cast(lifecycleService);
                    break;
                case Persistence:
                    if (null == persistenceService)
                    {
                        throw new ServiceNotInstalledException(serviceClassification, IPersistenceService.class);
                    }
                    result = serviceInterface.cast(persistenceService);
                    break;
                case Messaging:
                    if (null == messageService)
                    {
                        throw new ServiceNotInstalledException(serviceClassification, IMessageService.class);
                    }
                    result = serviceInterface.cast(messageService);
                    break;
                case Theme:
                    if (null == themeService)
                    {
                        throw new ServiceNotInstalledException(serviceClassification, IThemeService.class);
                    }
                    result = serviceInterface.cast(themeService);
                    break;
                case ApplicationConfiguration:
                    result = serviceInterface.cast(appConfigService);
                    break;
                case Custom:
                    Integer serviceIntfHashCode = serviceInterface.hashCode();

                    if (customServiceMap.containsKey(serviceIntfHashCode))
                    {
                        result = serviceInterface.cast(customServiceMap.get(serviceIntfHashCode));
                    } else
                    {
                        // first request
                        try
                        {
                            //noinspection unchecked
                            result = (T) initializeService(serviceInterface.getCanonicalName(), serviceInterface);
                        } catch (ServiceInitializationException e)
                        {
                            this.delegate
                                    .getEngine(IExtendedRuntimeEngine.class)
                                    .onCustomPlatformServiceInitializationException(e);
                        }
                    }

                    break;
            }
        }

        if (null == result)
        {
            throw new ServiceNotInstalledException(serviceClassification, serviceInterface);
        }

        return result;
    }

}
